﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MugenInjection;
using MugenInjection.Attributes;
using MugenInjection.Core.Components;
using MugenInjection.Infrastructure;
using MugenInjection.Interface;
using MugenInjection.Interface.Components;
using MugenInjection.Parameters;
using MugenInjectionTest.Components;
using Should;

namespace MugenInjectionTest.Converters
{
#if NOLAZY
    #region Lazy

        public class Lazy<T> where T : class
        {
            private readonly Func<T> _factory;
            private T _value;

            /// <summary>
            /// Initializes a new instance of the <see cref="T:System.Object"/> class.
            /// </summary>
            public Lazy(Func<T> factory)
            {
                _factory = factory;
            }

            public T Value
            {
                get { return _value ?? (_value = _factory()); }
            }
        }

    #endregion
#endif

    [TestClass]
    public class FactoryAndLazyConverterTest : ConverterTestBase<FactoryAndLazyConverter>
    {
        #region Delegates

        public delegate T CustomFunc<out T>();

        public delegate TResult CustomFunc<in T1, out TResult>(T1 t);

        public delegate TResult CustomFunc<in T1, in T2, out TResult>(T1 t1, T2 t2);

        #endregion

        #region Internal methods

        protected void TestSimpleFactoryInjectInternal<T>(T constValue,
                                                          Action<FactoryAndLazyConverter> configure = null)
        {
            var type = typeof(T);
            ConstructorInfo constructorInfo = typeof(FactoryInjectClass).GetConstructor(new[] { type });
            const string value = "value";
            using (MugenInjector injector = GetInjector())
            {
                var converter = CreateNew();
                var bindingActivatorComponent = injector.Components.BindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                bindingActivatorComponent.AddConverter(converter);
                if (configure != null)
                    configure(converter);

                injector.Bind<string>().ToConstant(value).WhenInto(typeof(FactoryInjectClass));
                IBindingContext bindingContext = injector.CreateContext(typeof(FactoryInjectClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());

                object[] parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                parameters.Length.ShouldEqual(1);
                parameters[0].ShouldBeType(type);

                var func = parameters[0] as Delegate;
                func.ShouldNotBeNull();
                func.DynamicInvoke().ShouldEqual(value);

                injector.Bind<T>().ToConstant(constValue);
                parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                constValue.ShouldEqual(parameters[0]);
            }
        }

        protected void TestParameterFactoryInjectInternal<T>(T constValue,
                                                             Action<FactoryAndLazyConverter> configure = null)
        {
            var type = typeof(T);
            ConstructorInfo constructorInfo = typeof(FactoryInjectClass).GetConstructor(new[] { type });
            using (MugenInjector injector = GetInjector())
            {
                bool isInvoke = false;
                var methodConst = new SimpleParameterClass(null, null);
                var parameter1 = new ConstructorParameter("test", "test");
                var parameter2 = new PropertyParameter("test", "test");

                var converter = CreateNew();
                var bindingActivatorComponent = injector.Components.BindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                bindingActivatorComponent.AddConverter(converter);
                if (configure != null)
                    configure(converter);

                injector.Bind<SimpleParameterClass>().ToMethod(context =>
                {
                    isInvoke = true;
                    context.Parameters.Contains(parameter1).ShouldBeTrue();
                    context.Parameters.Contains(parameter2).ShouldBeTrue();
                    return methodConst;
                });
                IBindingContext bindingContext = injector.CreateContext(typeof(FactoryInjectClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());
                object[] parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                parameters.Length.ShouldEqual(1);
                parameters[0].ShouldBeType(type);

                var func = parameters[0] as Delegate;
                func.ShouldNotBeNull();
                func.DynamicInvoke(new object[] { new IInjectionParameter[] { parameter1, parameter2 } })
                    .ShouldEqual(methodConst);
                isInvoke.ShouldBeTrue();

                injector.Bind<T>().ToConstant(constValue);
                parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                constValue.ShouldEqual(parameters[0]);
            }
        }

        protected void TestSpecialParameterFactoryInjectInternal<T>(T constValue,
                                                                    Action<FactoryAndLazyConverter> configure = null)
        {
            var type = typeof(T);
            ConstructorInfo constructorInfo = typeof(FactoryInjectClass).GetConstructor(new[] { type });
            using (MugenInjector injector = GetInjector())
            {
                bool isInvoke = false;
                var methodConst = new SimpleParameterClass(null, null);
                const string keyName = "test";

                var converter = CreateNew();
                var bindingActivatorComponent = injector.Components.BindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                bindingActivatorComponent.AddConverter(converter);
                if (configure != null)
                    configure(converter);

                injector.Bind<SimpleParameterClass>().ToMethod(context =>
                {
                    isInvoke = true;
                    context.SpecialParameters[keyName].ShouldEqual(keyName);
                    return methodConst;
                });
                IBindingContext bindingContext = injector.CreateContext(typeof(FactoryInjectClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());
                object[] parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                parameters.Length.ShouldEqual(1);
                parameters[0].ShouldBeType(type);

                var func = parameters[0] as Delegate;
                func.ShouldNotBeNull();
                func.DynamicInvoke(new DictionarySettingsImpl(new Dictionary<string, object> { { keyName, keyName } })).ShouldEqual(methodConst);
                isInvoke.ShouldBeTrue();

                injector.Bind<T>().ToConstant(constValue);
                parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                constValue.ShouldEqual(parameters[0]);
            }
        }

        protected void TestSpecialParameterAndParameterFactoryInjectInternal<T>(T constDelegate,
                                                                                Action<FactoryAndLazyConverter>
                                                                                    configure = null)
        {
            var type = typeof(T);
            ConstructorInfo constructorInfo = typeof(FactoryInjectClass).GetConstructor(new[] { type });
            using (MugenInjector injector = GetInjector())
            {
                bool isInvoke = false;
                var methodConst = new SimpleParameterClass(null, null);
                const string keyName = "test";
                var parameter1 = new ConstructorParameter("test", "test");
                var parameter2 = new PropertyParameter("test", "test");

                var converter = CreateNew();
                var bindingActivatorComponent = injector.Components.BindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                bindingActivatorComponent.AddConverter(converter);
                if (configure != null)
                    configure(converter);

                injector.Bind<SimpleParameterClass>().ToMethod(context =>
                {
                    isInvoke = true;

                    context.Parameters.Contains(parameter1).ShouldBeTrue();
                    context.Parameters.Contains(parameter2).ShouldBeTrue();
                    context.SpecialParameters[keyName].ShouldEqual(keyName);
                    return methodConst;
                });
                IBindingContext bindingContext = injector.CreateContext(typeof(FactoryInjectClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());
                object[] parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                Assert.AreEqual(parameters.Length, 1);
                parameters.Length.ShouldEqual(1);
                parameters[0].ShouldBeType(type);

                var func = parameters[0] as Delegate;
                func.ShouldNotBeNull();
                func.DynamicInvoke(new IInjectionParameter[] { parameter1, parameter2 },
                                   new DictionarySettingsImpl(new Dictionary<string, object> { { keyName, keyName } }))
                    .ShouldEqual(methodConst);
                isInvoke.ShouldBeTrue();

                injector.Bind<T>().ToConstant(constDelegate);
                parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                constDelegate.ShouldEqual(parameters[0]);
            }
        }

        protected void TestSimpleLazyInjectInternal<T>(T constValue, Func<T, object> getValue,
                                                       Action<FactoryAndLazyConverter> configure = null)
        {
            var type = typeof(T);
            ConstructorInfo constructorInfo = typeof(LazyInjectClass).GetConstructor(new[] { type });
            const string value = "value";
            using (MugenInjector injector = GetInjector())
            {
                var converter = CreateNew();
                var bindingActivatorComponent = injector.Components.BindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                bindingActivatorComponent.AddConverter(converter);
                if (configure != null)
                    configure(converter);

                injector.Bind<string>().ToConstant(value).WhenInto(typeof(LazyInjectClass));
                IBindingContext bindingContext = injector.CreateContext(typeof(LazyInjectClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());

                object[] parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                Assert.AreEqual(parameters.Length, 1);
                parameters.Length.ShouldEqual(1);
                getValue((T)parameters[0]).ShouldEqual(value);

                injector.Bind<T>().ToConstant(constValue);
                parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                constValue.ShouldEqual(parameters[0]);
            }
        }

        protected void TestNamedLazyInjectInternal<T>(T constValue, Func<T, object> getValue,
                                                       Action<FactoryAndLazyConverter> configure = null)
        {
            ConstructorInfo constructorInfo = typeof(LazyInjectClass).GetConstructor(new[] { typeof(T) });
            var value = new SimpleParameterClass(null, null);
            using (MugenInjector injector = GetInjector())
            {
                var converter = CreateNew();
                var bindingActivatorComponent = injector.Components.BindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                bindingActivatorComponent.AddConverter(converter);
                if (configure != null)
                    configure(converter);

                injector.Bind<SimpleParameterClass>().ToConstant(value).WhenInto(typeof(LazyInjectClass)).NamedBinding("test");
                IBindingContext bindingContext = injector.CreateContext(typeof(LazyInjectClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());

                object[] parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                Assert.AreEqual(parameters.Length, 1);
                parameters.Length.ShouldEqual(1);
                getValue((T)parameters[0]).ShouldEqual(value);

                injector.Bind<T>().ToConstant(constValue).NamedBinding("test");
                parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                constValue.ShouldEqual(parameters[0]);
            }
        }

        #endregion

        [TestMethod]
        public void SimpleFactoryInjectTest()
        {
            TestSimpleFactoryInjectInternal(new Func<string>(() => "tset"));
        }

        [TestMethod]
        public void ParameterFactoryInjectTest()
        {
            TestParameterFactoryInjectInternal(
                new Func<IEnumerable<IInjectionParameter>, SimpleParameterClass>(enumerable => null));
        }

        [TestMethod]
        public void SpecialParameterFactoryInjectTest()
        {
            TestSpecialParameterFactoryInjectInternal(
                new Func<ISettings, SimpleParameterClass>(enumerable => null));
        }

        [TestMethod]
        public void SpecialParameterAndParameterFactoryInjectTest()
        {
            TestSpecialParameterAndParameterFactoryInjectInternal(
                new Func<IEnumerable<IInjectionParameter>, ISettings, SimpleParameterClass>(
                    (p, p1) => null));
        }

        [TestMethod]
        public void CustomFactoryDelegateTest()
        {
            TestSimpleFactoryInjectInternal(new CustomFunc<string>(() => "tset"),
                                            component => component.FactoryDelegateTypes.Add(typeof(CustomFunc<>)));
        }

        [TestMethod]
        public void CustomParameterFactoryInjectTest()
        {
            TestParameterFactoryInjectInternal(
                new CustomFunc<IEnumerable<IInjectionParameter>, SimpleParameterClass>(enumerable => null),
                component =>
                {
                    component.FactoryDelegateTypes.Add(typeof(CustomFunc<>));
                    component.FactoryDelegateTypes.Add(typeof(CustomFunc<,>));
                    component.FactoryDelegateTypes.Add(typeof(CustomFunc<,,>));
                });
        }

        [TestMethod]
        public void CustomSpecialParameterFactoryInjectTest()
        {
            TestSpecialParameterFactoryInjectInternal(
                new CustomFunc<ISettings, SimpleParameterClass>(enumerable => null),
                component =>
                {
                    component.FactoryDelegateTypes.Add(typeof(CustomFunc<>));
                    component.FactoryDelegateTypes.Add(typeof(CustomFunc<,>));
                    component.FactoryDelegateTypes.Add(typeof(CustomFunc<,,>));
                });
        }

        [TestMethod]
        public void CustomSpecialParameterAndParameterFactoryInjectTest()
        {
            TestSpecialParameterAndParameterFactoryInjectInternal(
                new CustomFunc<IEnumerable<IInjectionParameter>, ISettings, SimpleParameterClass>(
                    (p, p1) => null), component =>
                    {
                        component.FactoryDelegateTypes.Add(typeof(CustomFunc<>));
                        component.FactoryDelegateTypes.Add(typeof(CustomFunc<,>));
                        component.FactoryDelegateTypes.Add(typeof(CustomFunc<,,>));
                    });
        }

        [TestMethod]
        public void SimpleLazyInjectTest()
        {
            TestSimpleLazyInjectInternal(new Lazy<string>(() => "test"), lazy => lazy.Value);
        }

        [TestMethod]
        public void CustomLazyInjectTest()
        {
            TestSimpleLazyInjectInternal(new CustomLazy<string>(() => "test"), lazy => lazy.Value,
                                         component => component.LazyDelegateTypes.Add(typeof(CustomLazy<>)));
        }

        [TestMethod]
        public void NamedSimpleLazyInjectTest()
        {
            TestNamedLazyInjectInternal(new Lazy<SimpleParameterClass>(() => null), lazy => lazy.Value);
        }

        [TestMethod]
        public void NamedCustomLazyInjectTest()
        {
            TestNamedLazyInjectInternal(new CustomLazy<SimpleParameterClass>(() => null), lazy => lazy.Value,
                                        component => component.LazyDelegateTypes.Add(typeof(CustomLazy<>)));
        }

        [TestMethod]
        public void FactoryWithConstructorArgumentsTest()
        {
            using (var injector = GetInjector())
            {
                const string value = "value";
                var func = injector.Get<Func<string, IDisposable, ConstructorParameterTest>>();
                var parameterTest = func(value, injector);
                parameterTest.Value1.ShouldEqual(value);
                parameterTest.Value2.ShouldEqual(injector);
            }
        }

        [TestMethod]
        public void LazyWithConstructorArgumentsTest()
        {
            using (var injector = GetInjector())
            {
                const string value = "value";
                var func = injector.Get<Func<string, IDisposable, Lazy<ConstructorParameterTest>>>();
                var parameterTest = func(value, injector).Value;
                parameterTest.Value1.ShouldEqual(value);
                parameterTest.Value2.ShouldEqual(injector);
            }
        }

        [TestMethod]
        public void FactoryChildInjectorTest()
        {
            const string value = "one";
            const string childValue = "two";
            using (MugenInjector injector = GetInjector())
            {
                injector.Bind<string>().ToConstant(value);
                using (var child = (MugenInjector)injector.CreateChild())
                {
                    child.Bind<string>().ToConstant(childValue);

                    injector.Get<Func<string>>()().ShouldEqual(value);
                    var func = child.Get<Func<string>>();
                    func().ShouldEqual(childValue);
                }
            }
        }

        [TestMethod]
        public void LazyChildInjectorTest()
        {
            const string value = "one";
            const string childValue = "two";
            using (MugenInjector injector = GetInjector())
            {
                injector.Bind<string>().ToConstant(value);
                using (var child = (MugenInjector)injector.CreateChild())
                {
                    child.Bind<string>().ToConstant(childValue);

                    injector.Get<Lazy<string>>().Value.ShouldEqual(value);
                    child.Get<Lazy<string>>().Value.ShouldEqual(childValue);
                }
            }
        }

        [TestMethod]
        public void FactoryChildInjectorResolveSettingTest()
        {
            const string value = "one";
            const string childValue = "two";
            using (MugenInjector injector = GetInjector())
            {
                injector.Bind<TestClass>().ToSelf();
                injector.Bind<string>().ToConstant(value);
                using (var child = (MugenInjector)injector.CreateChild())
                {
                    child.Bind<string>().ToConstant(childValue);

                    var testClass = injector.Get<TestClass>();
                    testClass.St.ShouldEqual(value);
                    testClass.Lazy.Value.ShouldEqual(value);
                    testClass.Func().ShouldEqual(value);
                    
                    testClass = child.Get<TestClass>();
                    testClass.St.ShouldEqual(value);
                    testClass.Lazy.Value.ShouldEqual(value);
                    testClass.Func().ShouldEqual(value);
                    
                    child.Settings.UseParentToResolve = false;

                    testClass = child.Get<TestClass>();
                    testClass.St.ShouldEqual(childValue);
                    testClass.Lazy.Value.ShouldEqual(childValue);
                    testClass.Func().ShouldEqual(childValue);
                    
                    testClass = injector.Get<TestClass>();
                    testClass.St.ShouldEqual(value);
                    testClass.Lazy.Value.ShouldEqual(value);
                    testClass.Func().ShouldEqual(value);
                }
            }
        }

        #region Nested type: TestClass

        public class TestClass
        {
            public string St { get; set; }
            public Func<string> Func { get; set; }
            public Lazy<string> Lazy { get; set; }

            /// <summary>
            /// Initializes a new instance of the <see cref="TestClass"/> class.
            /// </summary>
            public TestClass(string st, Func<string> func, Lazy<string> lazy)
            {
                St = st;
                Func = func;
                Lazy = lazy;
            }
        }

        #endregion

        #region Nested type: ConstructorParameterTest

        public class ConstructorParameterTest
        {
            public string Value1 { get; set; }

            public IDisposable Value2 { get; set; }

            /// <summary>
            /// Initializes a new instance of the <see cref="ConstructorParameterTest"/> class.
            /// </summary>
            public ConstructorParameterTest(string value1, IDisposable value2)
            {
                Value1 = value1;
                Value2 = value2;
            }
        }

        #endregion

        #region Nested type: CustomLazy

        public class CustomLazy<T> where T : class
        {
            private readonly Func<T> _factory;
            private T _value;

            /// <summary>
            /// Initializes a new instance of the <see cref="T:System.Object"/> class.
            /// </summary>
            public CustomLazy(Func<T> factory)
            {
                _factory = factory;
            }

            public T Value
            {
                get { return _value ?? (_value = _factory()); }
            }
        }

        #endregion

        #region Nested type: FactoryInjectClass

        public class FactoryInjectClass
        {
            public FactoryInjectClass(CustomFunc<string> simpleFactory)
            {
            }

            public FactoryInjectClass(CustomFunc<IEnumerable<IInjectionParameter>, SimpleParameterClass> parameterFunc)
            {
            }

            public FactoryInjectClass(CustomFunc<ISettings, SimpleParameterClass> parameterFunc)
            {
            }

            public FactoryInjectClass(
                CustomFunc<IEnumerable<IInjectionParameter>, ISettings, SimpleParameterClass>
                    parameterFunc)
            {
            }

            public FactoryInjectClass(Func<string> simpleFactory)
            {
            }

            public FactoryInjectClass([NamedParameter("test")] Func<SimpleParameterClass> simpleFactory)
            {
            }


            public FactoryInjectClass(Func<IEnumerable<IInjectionParameter>, SimpleParameterClass> parameterFunc)
            {
            }

            public FactoryInjectClass(Func<ISettings, SimpleParameterClass> parameterFunc)
            {
            }

            public FactoryInjectClass(
                Func<IEnumerable<IInjectionParameter>, ISettings, SimpleParameterClass> parameterFunc)
            {
            }
        }

        #endregion

        #region Nested type: LazyInjectClass

        public class LazyInjectClass
        {
            public LazyInjectClass(Lazy<string> lazy)
            {
            }

            public LazyInjectClass(CustomLazy<string> lazy)
            {
            }

            public LazyInjectClass([NamedParameter("test")] Lazy<SimpleParameterClass> lazy)
            {
            }

            public LazyInjectClass([NamedParameter("test")] CustomLazy<SimpleParameterClass> lazy)
            {
            }
        }

        #endregion

        #region Overrides of ConverterTestBase<FactoryAndLazyConverter>

        protected override FactoryAndLazyConverter CreateNew()
        {
            return new FactoryAndLazyConverter();
        }

        #endregion
    }
}
